Topic 3 Multithreading (2)

Leedehai
Monday, May 1, 2017
Firday, May 5, 2017

3.3 C++ form of multithreading: <thread>

3.3.1 The old-school C form:

/* C */ #include <stdio.h> #include <pthread.h> /* thread routine */ /* Note the return and arg types are all "void *" */ void *recharge(void *unused) { printf("I recharge by spending time alone.\n"); return NULL; } int main() { pthread_t introverts[6]; for (size_t i = 0; i < 6; i++) { pthread_create(&introverts[i], NULL, recharge, NULL); } for (size_t i = 0; i < 6; i++) { pthread_join(introverts[i], NULL); } return 0; }

3.3.2 The C++ form:

using namespace std; is omitted in C++ code afterwards.

/* C++11 */ #include <iostream> #include <thread> /* CS110 iomanipulators (oslock, osunlock) used to lock down streams */ #include "ostreamlock.h" using namespace std; /* Note the return type is "void" */ void recharge() { cout << oslock << "I recharge by spending time alone." << endl << osunlock; return; } int main() { /* declare 6 thread objects (handles) */ thread introverts[6]; /* install thread routine to these threads */ for (thread &t : introverts) { t = thread(recharge); } /* join: block, wait, reap zombie threads */ /* note you need the "&" below, since thread class's * assignment operator is deleted */ for (thread &t : introverts) { t.join(); } return 0; }

C++ prototypes explained:

SIDE NOTE:
The method thread &operator=(thread &&t) takes in an rvalue reference. A effect of this design is that, after the thread object t on right hand side is assigned to the left hand side, the original t is gutted: as if it were a zero-argument constructed, empty, detached thread object.
Such mechanism is suitable if the right hand size object takes considerable effort to construct or clone.
It is unlike normal a = b assignment, after which b is intact and a is a copy of b.

3.3.3 Another C++ example: using variadic arguments

/* C++11 */ #include ... /* Note the return type is "void" and we pass in an arg to it*/ void greet(size_t id) { for (size_t i = 0; i < id; i++) { cout << oslock << "Greeter #" << id << " says 'Hello!'" << endl << osunlock; } cout << oslock << "Greeter #" << id << " has issued all of his hellos, " << "so he goes home!" << endl << osunlock; } int main(int argc, char *argv[]) { thread greeters[6]; for (size_t i = 0; i < 6; i++) { /* Note that the variadic arg is passed in (one arg here): thread (pRoutine, args...); */ greeters[i] = thread(greet, i + 1); /* Pay attention to avoid the race condition as discussed in 3.2.1 */ } for (thread& greeter: greeters) { greeter.join(); } return 0; }

3.4 Race condition: critical region, mutex (KOB)

3.4.1 Buggy ticket selling:

/* Buggy */ #include <iostream> #include <thread> #include "ostreamlock.h" using namespace std; static int numTickets = 3; /* threads cannot share automatic variables */ 7 void ticketAgent(size_t id) { 8 while (numTickets > 0) { 9 numTickets--; 10 cout << oslock 11 << "Ticket agent " << id << " sold one ticket," 12 << " there are " << numTickets 13 << " more to sell!" << endl 14 << osunlock; 15 } 16} int main() { thread agents[2]; for (size_t i = 0; i < 2; i++) { agents[i] = thread(ticketAgent, 101 + i); /* agent 101, 102 */ } for (thread &a : agents) { /* block and wait for each thread */ a.join(); } cout << "End of business day!" << endl; return 0; }

There are two problems caused by the unpredictability of the scheduler.

Pause and think: which two?

A race condition happens when the processes or threads depend on some shared state (numTickets's value here).
Operations upon shared states are critical code sections (critical regions) that must be mutually exclusive. That is, when one steps into the critical region, the others should be prevented to do the same until the first one steps out.

3.4.2 Solution: mutex and locking

The fixed version:

/* Problem fixed */ #include <iostream> #include <thread> #include "ostreamlock.h" #include <mutex> /* for mutex class */ using namespace std; static int numTickets = 3; static mutex ticketLock; /* global, shared among threads */ 7 void ticketAgent(size_t id) { 8* while (true) { 8+ ticketLock.lock(); /* lock */ 9 numTickets--; 10 cout << oslock 11 << "Ticket agent " << id << " sold one ticket," 12 << " there are " << numTickets 13 << " more to sell!" << endl 14 << osunlock; 14+ if (numTickets == 0) break; 14++ ticketLock.unlock(); /* unlock */ 15 } 16} int main() { thread agents[2]; for (size_t i = 0; i < 2; i++) { agents[i] = thread(ticketAgent, 101 + i); /* agent 101, 102 */ } for (thread &a : agents) { /* block and wait for each thread */ a.join(); } cout << "End of business day!" << endl; return 0; }

Code explained:

Appendix: the implementation of oslock and osunlock

/* ostreamlock.h */ #include <ostream> std::ostream& oslock(std::ostream& os); std::ostream& osunlock(std::ostream& os);
/* ostreamlock.cc */ #include <ostream> #include <iostream> #include <mutex> #include <memory> #include <map> #include "ostreamlock.h" using namespace std; static mutex mapLock; static map<ostream *, unique_ptr<mutex>> streamLocks; ostream& oslock(ostream& os) { ostream *ostreamToLock = &os; if (ostreamToLock == &cerr) ostreamToLock = &cout; mapLock.lock(); unique_ptr<mutex>& up = streamLocks[ostreamToLock]; if (up == nullptr) { up.reset(new mutex); } mapLock.unlock(); up->lock(); return os; } ostream& osunlock(ostream& os) { ostream *ostreamToLock = &os; if (ostreamToLock == &cerr) ostreamToLock = &cout; mapLock.lock(); auto found = streamLocks.find(ostreamToLock); mapLock.unlock(); if (found == streamLocks.end()) throw "unlock inserted into stream that has never been locked."; unique_ptr<mutex>& up = found->second; up->unlock(); return os; }
EOF